﻿/**
 * Copyright (c) 2008 Justin Etheredge http://www.codethinked.com
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Microsoft Public License (Ms-PL)
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/ms-pl.html
 * If redistributing this code, this entire header must remain intact.
 * 
 * Some of the code in this application has been taken 
 * from the following sources (all of which are licensed under
 * the Ms-PL):
 * Matt Warren's Blog: http://blogs.msdn.com/mattwar/
 * MSDN: http://msdn2.microsoft.com/en-us/library/bb546158.aspx
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Amazon.SimpleDB;
using Amazon.SimpleDB.Model;
using System.Security;

namespace LinqToSimpleDB
{
  public class SimpleDBContext
  {
    private static string _staticAccessKeyId = string.Empty;
    private static string _staticSecretAccessKey = string.Empty;
    private Query<SimpleDBItem> _query = null;
    private string _domain = null;
    private int _precision = 40;
    private int _scale = 10;
    private string _accessKeyId = string.Empty;
    private string _secretAccessKey = string.Empty;

    private List<SimpleDBItem> _items = null;

    private AmazonSimpleDB _simpleDBClient = null;

    public Query<SimpleDBItem> Domain
    {
      get
      {
        if (_query == null)
        {
          _query = new Query<SimpleDBItem>(new SimpleDBQueryProvider(this));
        }
        return _query;
      }
    }

    public string DomainName
    {
      get { return _domain; }
    }

    public AmazonSimpleDB SimpleDBClient
    {
      get
      {
        if (_simpleDBClient == null)
        {
          _simpleDBClient = new AmazonSimpleDBClient(_accessKeyId, _secretAccessKey);
        }
        return _simpleDBClient;
      }
    }

    public int Precision
    {
      get { return _precision; }
    }

    public int Scale
    {
      get { return _scale; }
    }

    private List<SimpleDBItem> Items
    {
      get
      {
        if (_items == null)
        {
          _items = new List<SimpleDBItem>();
        }
        return _items;
      }
    }

    internal string AccessKeyId
    {
      get { return _accessKeyId; }
    }

    internal string SecretAccessKey
    {
      get { return _secretAccessKey; }
    }

    internal static string StaticAccessKeyId
    {
      get { return _staticAccessKeyId; }
    }

    internal static string StaticSecretAccessKey
    {
      get { return _staticSecretAccessKey; }
    }

    private SimpleDBContext() { }

    /// <summary>
    /// Creates a SimpleDBContext by specifying which domain you are going to
    /// be accessing.
    /// </summary>
    /// <param name="domain">Specifies which SimpleDB Domain you will be accessing.</param>
    /// <param name="precision">Specifies the total length of numbers in this domain including
    /// all whole and decimal places.</param>
    /// <param name="scale">Specifies how many decimal places are stored with numbers.</param>
    private SimpleDBContext(string accessKeyId, string secretAccessKey, string domain, int precision, int scale)
    {
      if (String.IsNullOrEmpty(accessKeyId))
      {
        throw new ArgumentNullException("accessKeyId");
      }
      if (String.IsNullOrEmpty(secretAccessKey))
      {
        throw new ArgumentNullException("secretAccessKey");
      }
      if (String.IsNullOrEmpty(domain))
      {
        throw new ArgumentNullException("domain");
      }
      if (precision <= 0)
      {
        throw new ArgumentException("Must be greater than 0", "precision");
      }
      if (scale < 0)
      {
        throw new ArgumentException("Must be greater than or equal to 0", "scale");
      }
      _accessKeyId = accessKeyId;
      _secretAccessKey = secretAccessKey;
      _domain = domain;
      _precision = precision;
      _scale = scale;
    }

    private SimpleDBContext(string accessKeyId, string secretAccessKey, string domain) :
      this(accessKeyId, secretAccessKey, domain, 40, 10) { }

    public static void SetAccessKeys(string accessKeyId, string secretAccessKey)
    {
      _staticAccessKeyId = accessKeyId;
      _staticSecretAccessKey = secretAccessKey;
    }

    public static SimpleDBContext GetContext(string accessKeyId, string secretAccessKey, string domain, int precision, int scale)
    {
      return new SimpleDBContext(accessKeyId, secretAccessKey, domain, precision, scale);
    }

    public static SimpleDBContext GetContext(string domain, int precision, int scale)
    {
      return GetContext(_staticAccessKeyId, _staticSecretAccessKey, domain, precision, scale);
    }

    public static SimpleDBContext GetContext(AmazonSimpleDB simpleDBClient, string accessKeyId, string secretAccessKey, string domain, int precision, int scale)
    {
      SimpleDBContext result = new SimpleDBContext(accessKeyId, secretAccessKey, domain, precision, scale);
      result._simpleDBClient = simpleDBClient;
      return result;
    }

    public static SimpleDBContext GetContext(AmazonSimpleDB simpleDBClient, string domain, int precision, int scale)
    {
      return GetContext(simpleDBClient, _staticAccessKeyId, _staticSecretAccessKey, domain, precision, scale);
    }

    public static SimpleDBContext GetContext(string accessKeyId, string secretAccessKey, string domain)
    {
      return new SimpleDBContext(accessKeyId, secretAccessKey, domain);
    }

    public static SimpleDBContext GetContext(string domain)
    {
      return GetContext(_staticAccessKeyId, _staticSecretAccessKey, domain);
    }

    public static SimpleDBContext GetContext(AmazonSimpleDB simpleDBClient, string accessKeyId, string secretAccessKey, string domain)
    {
      SimpleDBContext result = new SimpleDBContext(accessKeyId, secretAccessKey, domain);
      result._simpleDBClient = simpleDBClient;
      return result;
    }

    public static SimpleDBContext GetContext(AmazonSimpleDB simpleDBClient, string domain)
    {
      return GetContext(simpleDBClient, _staticAccessKeyId, _staticSecretAccessKey, domain);
    }

    private static AmazonSimpleDB GetClient()
    {
      if (String.IsNullOrEmpty(_staticAccessKeyId))
      {
        throw new InvalidOperationException("Aws Access Key Id is not specified. Please call SetAccessKeys to set this.");
      }

      if (String.IsNullOrEmpty(_staticSecretAccessKey))
      {
        throw new InvalidOperationException("Aws Secrety Access Key is not specified. Please call SetAccessKeys to set this.");
      }
      return new AmazonSimpleDBClient(_staticAccessKeyId, _staticSecretAccessKey);
    }

    public static void CreateDomain(string name)
    {
      var action = new CreateDomain().WithDomainName(name);
      GetClient().CreateDomain(action);
    }

    public static void DeleteDomain(string name)
    {
      var action = new DeleteDomain().WithDomainName(name);
      GetClient().DeleteDomain(action);
    }

    public static List<string> ListDomains()
    {
      var action = new ListDomains();
      ListDomainsResponse response = GetClient().ListDomains(action);
      return response.ListDomainsResult.DomainName;
    }

    public void AddItem(SimpleDBItem item)
    {
      if (!Items.Contains(item))
      {
        item.Context = this;
        Items.Add(item);
      }
    }

    public void RemoveItem(SimpleDBItem item)
    {
      AddItem(item);
      ((IDeletable)item).MarkDeleted();
    }


    public void SubmitChanges()
    {
      List<SimpleDBItem> itemsToRemove = null;
      foreach (SimpleDBItem item in this.Items)
      {
        if (item.IsDeleted)
        {
          ProcessDeletedItem(item);
          if (itemsToRemove == null)
          {
            itemsToRemove = new List<SimpleDBItem>();
          }
          itemsToRemove.Add(item);
        }
        else
        {
          CheckForDeletedAttributes(item);
        }
      }

      if (itemsToRemove != null)
      {
        //remove deleted items from collection
        foreach (SimpleDBItem item in itemsToRemove)
        {
          this.Items.Remove(item);
        }
      }

      //last, save items to the db
      foreach (SimpleDBItem item in this.Items)
      {
        ProcessDirty(item);
        ((ISaveable)item).MarkSaved();
      }
    }

    private void ProcessDirty(SimpleDBItem item)
    {
      var action = new PutAttributes()
        .WithDomainName(_domain)
        .WithItemName(item.Name);

      foreach (SimpleDBAttribute attribute in item.Attributes)
      {
        if (attribute.IsDirty)
        {
          for (int i = 0; i < attribute.Values.Length; i++)
          {
            string value = null;
            if (attribute.Numeric[i])
            {
              value = SimpleDBNumericHelper.PadAndFormatNumber(attribute.NumericValues[i], this.Precision, this.Scale);
            }
            else
            {
              value = attribute.Values[i];
            }
            
            var apiAttribute = new ReplaceableAttribute()
                .WithName(attribute.Name)
                .WithValue(value)
                .WithReplace(true);
            action.Attribute.Add(apiAttribute);
          }
        }
      }

      if (action.Attribute.Count > 0)
      {
        this.SimpleDBClient.PutAttributes(action);
      }
    }

    private void ProcessDeletedItem(SimpleDBItem item)
    {
      var action = new DeleteAttributes()
          .WithDomainName(_domain)
          .WithItemName(item.Name);
      this.SimpleDBClient.DeleteAttributes(action);
    }

    private void CheckForDeletedAttributes(SimpleDBItem item)
    {
      List<SimpleDBAttribute> itemsToRemove = null;
      foreach (SimpleDBAttribute attribute in item.Attributes)
      {
        if (((IDeletable)attribute).IsDeleted)
        {
          this.DeleteAttribute(item.Name, attribute);
          if (itemsToRemove == null)
          {
            itemsToRemove = new List<SimpleDBAttribute>();
          }
          itemsToRemove.Add(attribute);
        }
      }

      if (itemsToRemove != null)
      {
        foreach (SimpleDBAttribute attribute in itemsToRemove)
        {
          item.Attributes.Remove(attribute);
        }
      }
    }

    internal IEnumerable<SimpleDBItem> Query(string query)
    {
      var action = new Query();
      action.DomainName = _domain;
      action.QueryExpression = query;
      QueryResponse response = this.SimpleDBClient.Query(action);
      var result = new List<SimpleDBItem>();
      foreach (string name in response.QueryResult.ItemName)
      {
        SimpleDBItem item = new SimpleDBItem(name);
        ((ISaveable)item).MarkSaved();
        item.Context = this;
        result.Add(item);
      }
      return result;
    }

    private void DeleteAttribute(string itemName, SimpleDBAttribute attribute)
    {
      var apiAttribute = new Amazon.SimpleDB.Model.Attribute()
        .WithName(attribute.Name);

      var action = new DeleteAttributes()
        .WithDomainName(_domain)
        .WithItemName(itemName)
        .WithAttribute(apiAttribute);
      this.SimpleDBClient.DeleteAttributes(action);
    }

    public SimpleDBItem LoadAttributes(SimpleDBItem item)
    {
      var action = new GetAttributes()
        .WithDomainName(_domain)
        .WithItemName(item.Name);

      item.Attributes.Clear();

      GetAttributesResponse response = this.SimpleDBClient.GetAttributes(action);
      foreach (Amazon.SimpleDB.Model.Attribute apiAttribute in response.GetAttributesResult.Attribute)
      {
        if (item.Attributes.Contains(apiAttribute.Name))
        {
          item.Attributes[apiAttribute.Name].AddValue(apiAttribute.Value);
        }
        else
        {
          SimpleDBAttribute attribute
            = new SimpleDBAttribute(apiAttribute.Name, apiAttribute.Value);
          item.Attributes.Add(attribute);
        }
      }
      return item;
    }

    public IEnumerable<SimpleDBItem> GetItemsByName(List<string> itemNames)
    {
      List<SimpleDBItem> items = new List<SimpleDBItem>(itemNames.Count);
      foreach (string itemName in itemNames)
      {
        SimpleDBItem item = new SimpleDBItem(itemName);
        LoadAttributes(item);
        if (item.Attributes.Count > 0)
        {
          items.Add(item);
        }        
      }
      return items;
    }
  }
}

